/*  
 * Copyright (C) 2006 University of South Australia
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 * 
 * Contributors: 
 *   Andrew Pennington
 *   Ross Smith
 *   Andrew Cunningham
 * 
 */
import java.awt.*;
import com.sun.j3d.utils.universe.*;
import javax.media.j3d.*;
import javax.swing.JFrame;
import javax.swing.WindowConstants;
import javax.vecmath.*;
import java.awt.event.*;
import java.awt.GraphicsConfiguration;
import com.sun.j3d.utils.geometry.*;

/* world imports */
import tracker.TrackerFastrak;
import tracker.TrackerPolhemus;
import tracker.TrackerSerialPort;
import tracker.TrackerThread;
import world.geometry.Clock;
import world.geometry.Goblet;
import world.geometry.Room;
import world.ClockBehavior;


public class WorldMain {
	
	/** the default width of the window when not in fullscreen mode */
	private static final int WINDOW_WIDTH = 640;
	
	/** the default height of the window when not in fullscreen mode */
	private static final int WINDOW_HEIGHT = 480;

	/** run the demo in fullscreen mode */
	private static boolean isFullScreen = false;

	/** is the user running in keyboard mode */
	private static boolean isKeyboardControl = false;
	
	private static boolean isMouseControl = false;
	
	/** Frame to hold the 3D world **/
	public JFrame worldFrame;
	
	/** geometry objects for the hands */
    public Hand hand1;
    public Hand hand2;
   
    /** Java universe */
    private SimpleUniverse u;
		
	/** the transform group containing the world. This is moved around the viewing platform
	 *  to simulate movement. */
	private TransformGroup worldBranch;

	/** the transform group containing the groups that appear to move with the user (ie. the hands).
	 * These remain static while the world moves around the viewing platform. */
	private TransformGroup staticBranch;
    

    /*
     * create your world in this method
	 */
    public void createWorld(TransformGroup world){

        /*Create a bounds for the background and lights and behaviors*/
        BoundingSphere bounds = new BoundingSphere(new Point3d(), 200.0);
      
        VR.log("Adding global lights");
        
        /* Set up the global lights */
        Color3f lColor1 = new Color3f(0.9f, 0.9f, 0.9f);
        Vector3f lDir1  = new Vector3f(0f, -0.5f, -1.0f);
        Vector3f lDir2  = new Vector3f(1.0f, -1.0f, 1.0f);
        Color3f alColor = new Color3f(0.4f, 0.4f, 0.5f);
        AmbientLight aLgt = new AmbientLight(alColor);
        aLgt.setInfluencingBounds(bounds);
        DirectionalLight lgt1 = new DirectionalLight(lColor1, lDir1);
		DirectionalLight lgt2 = new DirectionalLight(lColor1, lDir2);
		lgt1.setInfluencingBounds(bounds);
        lgt2.setInfluencingBounds(bounds);
        aLgt.setEnable(true);
        lgt1.setEnable(true);
        lgt2.setEnable(true);
        world.addChild(aLgt);
        world.addChild(lgt1);
        world.addChild(lgt2);
      
        /*Add the walls (just a box we live in)*/
        Room room = new Room();
        world.addChild(room);
        
        // add a VRML object (corner pillars for the room)
        VRObject pillars = VRObject.loadObjectFromFile("room");
        world.addChild(pillars);
        setCollidable(pillars);
        
        TransformGroup tg = new TransformGroup();
        Transform3D t3d = new Transform3D();
        t3d.setScale(0.5);
        tg.setTransform(t3d);
        
        /* add another VRML object (ball on a stand) */
        //VRObject ball1 = VRObject.loadObjectFromFile(new Box(), new Vector3d(0, 0, 0));
        Shape3D boxShape = new Shape3D(new BoxGeometry());
        TransformGroup boxGroup = new TransformGroup();
        Transform3D t3d2 = new Transform3D();
        boxGroup.getTransform(t3d2);
        t3d2.rotX(0.1);
        boxGroup.setTransform(t3d2);
        boxGroup.addChild(boxShape);
        VRObject box = new VRObject(boxGroup, "Box", new Vector3d());
        //tg.addChild(ball1);
        setCollidable(box);

        world.addChild(box);
        
        
        /* add another VRML object (another ball on a stand) */
        VRObject ball2 = VRObject.loadObjectFromFile("ball", new Vector3d(-5.0, -2.0, -5.0));
		//tg.addChild(ball2);
		setCollidable(ball2);
      
		world.addChild(ball2);
		
        /* add another VRML object (a coffee table) */
		VRObject table = VRObject.loadObjectFromFile("coffee_table", new Vector3d (1.0, -2.0, -9.0));
        world.addChild(table);
        setCollidable(table);
      
        /* add another VRML object (a bookcase) */
        VRObject bookcase = VRObject.loadObjectFromFile("bookcase", new Vector3d(-3.0, -0.9, -9.99));
        world.addChild(bookcase);
        setCollidable(bookcase);
       
        
        /* add a clock */
        Clock clockObject = new Clock();
        VRObject clock = new VRObject(clockObject, "Clock", new Vector3d(-3.0, 0.3, -9.9));
        world.addChild(clock);
        setCollidable(clock);
		
		ClockBehavior cb = new ClockBehavior(clockObject);
        cb.setSchedulingBounds (new BoundingSphere(new Point3d(0.0, 0.0, 0.0), Float.MAX_VALUE));
        world.addChild(cb);
		
        /*Add goblet*/
        Goblet goblet1_ = new Goblet ();
        TransformGroup goblet1TransformGroup = new TransformGroup ();
        goblet1TransformGroup.setUserData("Transform");
        Transform3D goblet1Transform = new Transform3D();
        Vector3d goblet1Location = new Vector3d(1.2, -1.3, -9.3);
        goblet1Transform.rotZ(Math.PI / 2.0);
        goblet1Transform.setTranslation(goblet1Location);
        goblet1Transform.setScale(0.3);
        goblet1TransformGroup.setTransform(goblet1Transform);
        goblet1TransformGroup.addChild(goblet1_);
        world.addChild(goblet1TransformGroup);
        setCollidable(goblet1TransformGroup);
        
        // add another goblet
        Goblet goblet2_ = new Goblet ();
        TransformGroup goblet2TransformGroup = new TransformGroup ();
        goblet2TransformGroup.setUserData ("Transform");
        Transform3D goblet2Transform = new Transform3D ();
        Vector3d goblet2Location = new Vector3d (0.4, -1.3, -9.15);
        goblet2Transform.rotZ (Math.PI  / 2.0);
        goblet2Transform.setTranslation (goblet2Location);
        goblet2Transform.setScale (0.3);
        goblet2TransformGroup.setTransform (goblet2Transform);
        goblet2TransformGroup.addChild (goblet2_);
        world.addChild(goblet2TransformGroup);
        //setCollidable(goblet2TransformGroup);
        
        VR.log("Finished building scene graph");
        
    }
    
    public WorldMain() {
        
    	VR.log(VR.getRevisionDetails());
    	
    	BranchGroup root = createSceneGraph();
    	createWorld(worldBranch);
    	initVREnvironment(root, staticBranch, worldBranch);
    	
    	/* create the jframe to contain the canvas3D and virtual world */
        worldFrame = new JFrame();
        worldFrame.setTitle("VR Demo " + VR.getVersion());
    	worldFrame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
    	worldFrame.setSize(WorldMain.WINDOW_WIDTH, WorldMain.WINDOW_HEIGHT);
        worldFrame.getContentPane().add(u.getCanvas());
    }

	public BranchGroup createSceneGraph() {

		VR.log("Creating Scene Graph");

		/*
		 *               root
		 *                |
		 *      +-------------------+
		 *      |                   |
		 *  worldBranch         staticBranch
		 *     /|\                 /\
		 *(world objects)      (user hands)
		 */

		/*Create the root of the branch graph and the root level transform group */
		BranchGroup root = new BranchGroup();

		/* Add all the world geometry to the world branch */
		worldBranch = new TransformGroup();
		worldBranch.setCapability(TransformGroup.ALLOW_TRANSFORM_READ);
		worldBranch.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE);
		worldBranch.setCapability(TransformGroup.ALLOW_LOCAL_TO_VWORLD_READ);
		worldBranch.setCapability(TransformGroup.ALLOW_CHILDREN_EXTEND);
		root.addChild(worldBranch);
		
		staticBranch = new TransformGroup();
		staticBranch.setCapability(BranchGroup.ALLOW_CHILDREN_EXTEND);
		root.addChild(staticBranch);
	
		VR.log("Finished building scene graph");

		return root;
	}
	
    /**
     * initialise the VR environment, including the viewing platform, trackers,
     * movement and look behaviors etc.
     */
    public void initVREnvironment(BranchGroup root, TransformGroup staticBranch, TransformGroup worldBranch){
        
    	GraphicsConfiguration config = SimpleUniverse.getPreferredConfiguration();

        FixedCanvas3D c = new FixedCanvas3D(config);
        
        // Create a simple scene and attach it to the virtual universe
        u = new SimpleUniverse(c);
      
        TransformGroup tg = u.getViewingPlatform().getViewPlatformTransform ();
        Transform3D tf = new Transform3D();
      
        // set the initial viewing position
        Point3d eye = new Point3d();
        Point3d centre = new Point3d(0, 0, -1.0);
        Vector3d up = new Vector3d(0, 1.5, 0);
        tf.lookAt(eye, centre, up); // just here to show how
        tf.invert();
        tg.setTransform(tf);
         
        View view = u.getViewer().getView();
        
        view.setBackClipDistance(100.0);
        view.setFrontClipDistance(0.01);
        view.setFieldOfView(1.0);
        int policy = view.getViewPolicy();

        /* log some debug messages */
        VR.log("frontClip distance = " + view.getFrontClipDistance(), VR.LOG_DEBUG);
        VR.log("fov = " + view.getFieldOfView(), VR.LOG_DEBUG);
        VR.log("Compatibility Mode Enabled: " + view.getCompatibilityModeEnable(), VR.LOG_DEBUG);
        VR.log("View policy: " + (policy == View.SCREEN_VIEW ? "SCREEN_VIEW" : "HMD_VIEW"), VR.LOG_DEBUG);
           
        PhysicalEnvironment physicalEnvironment = u.getViewer().getPhysicalEnvironment();
        ViewingPlatform vp = u.getViewingPlatform();
        TransformGroup viewPlatformTrans = vp.getViewPlatformTransform();
      
        /*Add text to the overlay canvas to display the current frame rate */
        Text2D fpsText = new Text2D("FPS = 00", new Color3f(0.4f,0.8f, 0), "", 28, Font.PLAIN);
        TransformGroup overlay = createText2DTransformGroup(fpsText, new Vector3d(50, 50, 0));

        /*Add text to the overlay canvas to display the current frame rate of the tracker */
        Text2D polhemusFpsText = new Text2D("Polhemus FPS = Not Attached", new Color3f(0.5f,0.8f,.0f), "", 18, Font.PLAIN);
        TransformGroup overlay1 = createText2DTransformGroup(polhemusFpsText, new Vector3d(50, 20, 0));
        
        /* Add the overlay group - Columbia University Classes */
		final OverlayGroup og = new OverlayGroup();
		BranchGroup obg = new BranchGroup();
		obg.addChild(overlay);
		obg.addChild(overlay1);
		og.setupOverlayGroup(c, viewPlatformTrans, obg);
		og.setOnNearClippingPlane(true);

		
		/* Add the hands to the static branch of the universe. When moving, the
		 * hands and viewing platform remain static while the world branch moves around us */
		hand1 = new Hand(); // right hand
		setCollidable(hand1);
        staticBranch.addChild(hand1);
        
        /* hand2 is by default set to null, as the keyboard control only uses 1 hand */
        hand2 = null;
        TrackerThread trackerDevice = null;
        ButtonDevice buttonController;

        if (isKeyboardControl){
         	
            VR.log("Keyboard Control Enabled", VR.LOG_NOTE);

        } else {

         	/*Use the Polhemus tracker to move around the world*/
        	VR.log("Polhemus Tracker mode Enabled", VR.LOG_NOTE);
         	
            TrackerSerialPort sp = new TrackerSerialPort();
            TrackerPolhemus tracker = sp.getPolhemusTracker();
            VR.log("Using " + tracker.getTrackerType() + " Polhemus tracker", VR.LOG_NOTE);
            
            /* Java virtual input device */
            trackerDevice = new TrackerThread(tracker, sp);
            trackerDevice.setProcessingMode(InputDevice.NON_BLOCKING);
            trackerDevice.initialize();
            trackerDevice.start();
            physicalEnvironment.addInputDevice(trackerDevice);
            
            /*Add the hand sensor to the java concept of tracking*/
            physicalEnvironment.setSensor(0, trackerDevice.getSensor(0)); /* head sensor */
            physicalEnvironment.setHeadIndex(0);
            physicalEnvironment.setSensor(1, trackerDevice.getSensor(1)); /* primary hand sensor */
            physicalEnvironment.setDominantHandIndex(1);

            /*If we are using the Fastrak (the two-handed tracker) then add another hand */
            if (tracker instanceof TrackerFastrak){
            	
            	hand2 = new Hand(); /* left hand */
            	setCollidable(hand2);
	            staticBranch.addChild(hand2);
	          	
	            physicalEnvironment.setSensor(2, trackerDevice.getSensor(2)); /* secondary hand sensor */
	            physicalEnvironment.setNonDominantHandIndex(2);
            }
        }
        
        /* create the object responsible for determining button state */
        if (isKeyboardControl || isMouseControl)
        	buttonController = ButtonDevice.getConventionalControlDeviceForAWTComponent(u.getViewer().getCanvas3D(), isMouseControl);
        else
            buttonController = ButtonDevice.getButtonDeviceForTracker(trackerDevice.getTracker());
        
        /* Add a behaviour to control the primary hand */
        Sensor sensor1 = physicalEnvironment.getSensorCount() >= 1 ? physicalEnvironment.getSensor(1) : null;
        HandBehaviorPrimary h1 = new HandBehaviorPrimary(hand1, sensor1, buttonController, viewPlatformTrans, worldBranch);
        h1.setSchedulingBounds(new BoundingSphere(new Point3d(), Double.MAX_VALUE));
        staticBranch.addChild(h1);
        
        /* add a behaviour to control the secondary hand (if available) */
        Sensor sensor2 = physicalEnvironment.getSensorCount() >= 2 ? physicalEnvironment.getSensor(2) : null;
        HandBehaviorSecondary h2 = new HandBehaviorSecondary(hand2, sensor2, buttonController, viewPlatformTrans, worldBranch);
        h2.setSchedulingBounds(new BoundingSphere(new Point3d(), Double.MAX_VALUE));
        staticBranch.addChild(h2);
        
        /* add the behavior to control viewing direction */
        HeadBehavior head;
        if (isKeyboardControl){
        	head = new KeyboardHeadBehavior(u.getViewer().getCanvas3D(), tg);
        } else {
        	head = new TrackerHeadBehavior(tg, physicalEnvironment.getSensor(0));
        }
        head.setSchedulingBounds(new BoundingSphere(new Point3d(), Double.MAX_VALUE));
        staticBranch.addChild(head);

        if (isMouseControl){
	        MouseLookBehavior myMouseRotate = new MouseLookBehavior(
	        		u.getViewer().getCanvas3D(), viewPlatformTrans);
	        myMouseRotate.setSchedulingBounds(new BoundingSphere(new Point3d(), 1000));
	        root.addChild(myMouseRotate);
        }
        
        /* adjust the height of the user depending on whether we're using the tracker or keyboard control */
        Transform3D t = new Transform3D();
        double y = -Double.parseDouble(VR.getDefaults().getProperty(isKeyboardControl ? "KEYBOARD_AVATAR_HEIGHT" : "POLHEMUS_AVATAR_HEIGHT", "0"));
		t.setTranslation(new Vector3d(0, y, 0));
		worldBranch.setTransform(t);
		
        /* add the behaviour to display the FPS of the system */
		FPSBehavior fps = new FPSBehavior(fpsText, polhemusFpsText, trackerDevice, og);
		staticBranch.addChild(fps);
         
        /*Add a key listener so we can exit when esc, q of Q are pressed*/
    	u.getViewer().getCanvas3D().addKeyListener(
			new KeyAdapter(){
	    	    public void keyTyped(KeyEvent e) {
	    	    	char c = e.getKeyChar();
	    	    	if (c == 'q' || c == 'Q' || c == 27){
	    	    		System.out.println("VR Virtual World Terminated");
	    	    		System.exit(0);
	    	    	}
	    	    }
			}
    	);

    	view.setMinimumFrameCycleTime(0);
    	root.compile();
        u.addBranchGraph(root);
        
        VR.log("Java3D Sensor Details:\n\tPolhemus Sensor 1: " + physicalEnvironment.getSensor(0) + "\n\tPolhemus Sensor 2: " + physicalEnvironment.getSensor(0) + "\n\tPolhemus Sensor 3: " + physicalEnvironment.getSensor(2));
    
    }
    
	/** sets an object to be collidable for hand interaction at the geometry
	 *  level. These computations are far slower but more accurate than a
	 *  bounding box method. */
	public static void setCollidable (Node node) {
		node.setCapability(Node.ENABLE_PICK_REPORTING);
		node.setCapability(Node.ENABLE_COLLISION_REPORTING);
		node.setCapability(Node.ALLOW_BOUNDS_READ);
		node.setCapability(Node.ALLOW_BOUNDS_WRITE);
		node.setCapability(Node.ALLOW_PICKABLE_READ);
		node.setCapability(Node.ALLOW_PICKABLE_WRITE);
		node.setCapability(Node.ALLOW_COLLIDABLE_READ);
		node.setCapability(Node.ALLOW_COLLIDABLE_WRITE);
		node.setCapability(Node.ALLOW_AUTO_COMPUTE_BOUNDS_READ);
		node.setCapability(Node.ALLOW_AUTO_COMPUTE_BOUNDS_WRITE);
		node.setCapability(Node.ALLOW_LOCAL_TO_VWORLD_READ);
	}

	/** creates a 2D HUD transform group */
    public TransformGroup createText2DTransformGroup(Text2D text, Vector3d offset){
    	text.setCapability(Text2D.ALLOW_APPEARANCE_READ);
    	text.setCapability(Text2D.ALLOW_APPEARANCE_WRITE);
    	text.getAppearance().setCapability(Appearance.ALLOW_TEXTURE_READ);
    	text.getAppearance().setCapability(Appearance.ALLOW_TEXTURE_WRITE);
	    TransformGroup overlay1 = new TransformGroup();
	    Transform3D overlaytrans1 = new Transform3D();
	    overlaytrans1.set(offset); // in pixel coordinates
	    overlaytrans1.setScale(256); // account for the scaling in Text2D
	    overlay1.setTransform(overlaytrans1);
	    overlay1.addChild(text);
	    return overlay1;
    }
	 
	public void destroy() {
    	u.cleanup();
    	exitFullscreen();
    }

    /** send the display to fullscreen mode. Taken from 
     * http://javaalmanac.com/egs/java.awt/screen_FullWin.html?l=rel 
     * [Link last checked 6 March 2007] */
    private static void enterFullscreen(Window win){
        GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
        GraphicsDevice gs = ge.getDefaultScreenDevice();
        VR.log("Fullscreen mode is " + (gs.isFullScreenSupported() ? "supported." : "emulated."));
        gs.setFullScreenWindow(win);
		win.validate();
    }
    
    private static void exitFullscreen(){
        GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
        GraphicsDevice gs = ge.getDefaultScreenDevice();
        gs.setFullScreenWindow(null);
    }
    
    
    public static WorldMain worldInstance = null;
    public static WorldMain getInstance(){
    	if (worldInstance == null)
    		worldInstance = new WorldMain();
    	return worldInstance;
    }
    
	
    public static void main(String[] args) {

    	if (args.length > 0){
			for (int i = 0; i < args.length; i++){
				if (args[i].equalsIgnoreCase("-keyboard"))
					isKeyboardControl = true;
				if (args[i].equalsIgnoreCase("-fullscreen"))
					isFullScreen = true;
				if (args[i].equalsIgnoreCase("-mouse")){
					isMouseControl = true;
					isKeyboardControl = true;
					isFullScreen = true;
				}
				if (args[i].equalsIgnoreCase("-nodebug"))
					VR.setLogLevel(3);
			}
		}
		
    	/*Create the world*/
    	WorldMain vrWorld = WorldMain.getInstance();
    	if (isFullScreen) {
    		vrWorld.worldFrame.setUndecorated(true);
    		enterFullscreen(vrWorld.worldFrame);
    	}
    	vrWorld.worldFrame.setVisible(true);
    }
}
